package util

import (
	"errors"
	"fmt"
	"math"
	"strings"

	"github.com/astaxie/beego"
	"github.com/pandatix/go-cvss/20"
	"github.com/pandatix/go-cvss/31"
	"github.com/pandatix/go-cvss/40"
)

const (
	defaultBaseScore     = 0
	vectorLength         = 8
	unChangedScope       = "SU"
	changedScope         = "SC"
	maxScore             = 10
	baseFactor           = 1.08
	unchangeImpactFactor = 6.42
	changeImpactFactor1  = 7.52
	changeImpactFactor2  = 0.029
	changeImpactFactor3  = 3.25
	changeImpactFactor4  = 0.02
	changeImpactFactor5  = 15
	iscFactor            = 1
	exFactor             = 8.22

	VectorNone = "None"

	length20    = 26
	length31    = 35
	length40Min = 54

	header20 = "CVSS:2.0/"
	header31 = "CVSS:3.1/"
	header40 = "CVSS:4.0/"
)

// BuildCVSSHeader add header to vector
func BuildCVSSHeader(v string) string {
	if v == "" {
		return ""
	}

	if len(v) >= length40Min {
		return header40 + v
	}

	return header31 + v
}

// ScoreAndVectorCheck check whether score and vector match
func ScoreAndVectorCheck(score float64, vector string) (bool, error) {
	if len(vector) == length20 {
		cvss20, err := gocvss20.ParseVector(header20 + vector)
		if err != nil {
			return false, err
		}

		return cvss20.BaseScore() == score, nil
	} else if len(vector) == length31 {
		cvss31, err := gocvss31.ParseVector(header31 + vector)
		if err != nil {
			return false, err
		}

		return cvss31.BaseScore() == score, nil
	} else if len(vector) >= length40Min {
		cvss40, err := gocvss40.ParseVector(header40 + vector)
		if err != nil {
			return false, err
		}

		return cvss40.Score() == score, nil
	} else {
		return false, errors.New("invalid vector")
	}
}

// Calculate CVSS v3 basescore based on vector
func CalculateCVSSV3BaseScore(vector string) float64 {
	if vector == "" {
		return 0
	}

	vectors := strings.Split(vector, "/")
	if len(vectors) != vectorLength {
		return defaultBaseScore
	}

	var dealVectors []string
	for _, v := range vectors {
		dv := strings.ReplaceAll(v, ":", "")
		dealVectors = append(dealVectors, dv)
	}

	scope := dealVectors[4]

	avScore := beego.AppConfig.DefaultFloat(fmt.Sprintf("metrics::%s", dealVectors[0]), defaultBaseScore)

	acScore := beego.AppConfig.DefaultFloat(fmt.Sprintf("metrics::%s", dealVectors[1]), defaultBaseScore)

	uiScore := beego.AppConfig.DefaultFloat(fmt.Sprintf("metrics::%s", dealVectors[3]), defaultBaseScore)

	cScore := beego.AppConfig.DefaultFloat(fmt.Sprintf("metrics::%s", dealVectors[5]), defaultBaseScore)

	iScore := beego.AppConfig.DefaultFloat(fmt.Sprintf("metrics::%s", dealVectors[6]), defaultBaseScore)

	aScore := beego.AppConfig.DefaultFloat(fmt.Sprintf("metrics::%s", dealVectors[7]), defaultBaseScore)

	iscBase := iscFactor - ((iscFactor - cScore) * (1 - iScore) * (1 - aScore))

	if scope == unChangedScope {
		prScore := beego.AppConfig.DefaultFloat(fmt.Sprintf("unchangedpr::%s", dealVectors[2]), defaultBaseScore)
		exploitabilitySubScore := exFactor * avScore * acScore * prScore * uiScore
		impactSubScore := unchangeImpactFactor * iscBase
		if impactSubScore <= 0 {
			return defaultBaseScore
		}

		miniNum := Minimum(exploitabilitySubScore+impactSubScore, maxScore)

		baseScore := Roundup(miniNum)

		return baseScore
	} else if scope == changedScope {
		prScore := beego.AppConfig.DefaultFloat(fmt.Sprintf("changedpr::%s", dealVectors[2]), defaultBaseScore)
		exploitabilitySubScore := exFactor * avScore * acScore * prScore * uiScore

		res := math.Pow(iscBase-changeImpactFactor4, float64(changeImpactFactor5))
		imapctSubScore := changeImpactFactor1*(iscBase-changeImpactFactor2) - changeImpactFactor3*res
		if imapctSubScore <= 0 {
			return defaultBaseScore
		}

		miniNum := Minimum(baseFactor*(exploitabilitySubScore+imapctSubScore), maxScore)

		baseScore := Roundup(miniNum)

		return baseScore
	}

	return defaultBaseScore
}

func Roundup(value float64) float64 {
	roundedValue := math.Ceil(value*10) / 10
	return roundedValue
}

func Minimum(a, b float64) float64 {
	if a < b {
		return a
	}
	return b
}
